﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Net;
using System.IO;
using System.Xml.Serialization;
using System.Xml.Linq;
using System.Diagnostics.Contracts;
using System.Text.RegularExpressions;
using ShredTheLog.Common;
using System.Threading;
using System.Globalization;
using System.Collections;
using ShredTheLog.Api.DataContracts;
using ShredTheLog.Api.DataContracts.TextChat;
using ShredTheLog.Api.DataContracts.Languages;
using ShredTheLog.Api;

namespace ShredTheLog.Model
{
    public sealed class ShredClient : IShredClient
    {
        private string _baseServer;
        private string _textChatServer;

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(!String.IsNullOrWhiteSpace(_baseServer));
            Contract.Invariant(!String.IsNullOrWhiteSpace(_textChatServer));
        }

        public ShredClient(string baseServer, string textChatServer)
        {
            Contract.Requires(!String.IsNullOrWhiteSpace(baseServer));
            Contract.Requires(!String.IsNullOrWhiteSpace(textChatServer));

            _baseServer = baseServer;
            _textChatServer = textChatServer;
        }

        private TimeSpan _updatePeriod = TimeSpan.FromSeconds(3);
        public TimeSpan UpdatePeriod { get { return _updatePeriod; } }

        public static Uri GetUri(string server, string path)
        {
            Contract.Requires(!String.IsNullOrWhiteSpace(server));
            Contract.Requires(path != null);
            Contract.Ensures(Contract.Result<Uri>() != null);

            return new Uri(String.Format(@"{0}/{1}", server, path));
        }

        public IObservable<IFullUserInfo> GetFullUserInfo(IUserInfo user)
        {
            var request = new GetFullUserInfoRequest(user);
            return
                SendRequest<FullUserInfo>(GetUri(_baseServer, "Members/GetFullMember.aspx"), request, null);
        }

        public IObservable<IBasicUserInfo[]> GetVoiceChatUsers()
        {
            var result =
                SendRequest<VoiceChatUserList>(GetUri(_baseServer, "VoiceChat/GetLobbyList.ashx"), null, "VoiceChatUserList")
                .Do(i => { if (i.NotDefined != null) throw new ApplicationException("Voice chat server is down."); })
                .Select(i => i.LobbyMembers.Users);
            Contract.Assume(result != null);
            return result;
        }

        public IObservable<IShortUserInfo[]> GetTextChatUsers()
        {
            var result =
                from i in SendRequest<TextChatUserList>(GetUri(_textChatServer, "Chat3/GetTCLobbyUsers.ashx"), null, "TextChatUserList")
                select i.Users ?? new ShortUserInfo[0];
            Contract.Assume(result != null);
            return result;
        }

        public IObservable<ILast15UserInfo[]> GetLast15Users()
        {
            var result =
                from i in SendRequest<Last15UserList>(GetUri(_textChatServer, "Members/GetLast15Members.aspx"), null, "Last15UserList")
                select i.Users ?? new Last15UserInfo[0];
            Contract.Assume(result != null);
            return result;
        }

        private int _lastUpdateDuration = 0;

        internal IObservable<TextChatUpdates> GetTextChatUpdates(string[] rooms, int updatesCount, int lastInUpdateID, Guid guid, int uid, ITextChatCommand[] commands)
        {
            Contract.Requires(rooms != null);
            Contract.Requires(updatesCount >= 1);
            Contract.Requires(lastInUpdateID >= 0);
            Contract.Requires(uid > 0);
            Contract.Requires(commands != null);
            Contract.Ensures(Contract.Result<IObservable<TextChatUpdates>>() != null);

            var _updateStartTime = DateTime.Now;
            var request = new GetTextChatUpdatesRequest(_lastUpdateDuration, uid, guid, lastInUpdateID, updatesCount, rooms, commands);
            var result = 
                SendRequest<TextChatUpdates>(GetUri(_textChatServer, "Chat3/GetTCUpdate.ashx"), request, null)
                .Finally(() => _lastUpdateDuration = (int)(DateTime.Now - _updateStartTime).TotalMilliseconds)
                .Do(updates => { if (updates.Exceptions.Any()) throw new AggregateException(updates.Exceptions.ToArray()); });
            Contract.Assume(result != null);
            return result;
        }

        public IObservable<ISessionCredentials> LogIn(string userName, string password)
        {
            var whenLoginDataReceived =
                SendRequest<LoginData>(GetUri(_textChatServer, "Members/Login.ashx"), new LogInRequest(userName, password), "LoginData")
                .Do(
                loginData =>
                {
                    if (loginData.UserNameOrPasswordIsIncorrect != null)
                        throw new InvalidCredentialsException();
                    else if (loginData.UserHasBeenBanned != null)
                        throw new FullBanException();
                    else if (loginData.UserHasBeenDeleted != null)
                        throw new UserDeletedException();
                    else if (loginData.Credentials == null)
                        throw new LoginException();
                });

            var result =
                from loginData in whenLoginDataReceived
                where loginData.Credentials != null
                select loginData.Credentials;
            Contract.Assume(result != null);
            return result;
        }

        public IObservable<IInitialTextChatInfo> JoinTextChat(ISessionCredentials user)
        {
            var request = new JoinTextChatRequest(user);
            var result =
                from i in SendRequest<InitialTextChatInfoWrapper>(GetUri(_textChatServer, "Chat3/JoinTC.ashx"), request, "Data")
                select i.InitialTextChatInfo;
            Contract.Assume(result != null);
            return result;
        }

        public IObservable<IInitialTextChatInfo> JoinTextChatAnonymously()
        {
            var result =
                SendRequest<InitialTextChatInfo>(GetUri(_textChatServer, "Chat3/JoinTC.ashx"), new JoinTextChatAnonymouslyRequest(), null);
            Contract.Assume(result != null);
            return result;
        }

        public ITextChat CreateTextChat(ISessionCredentials credentials, IInitialTextChatInfo initialInfo)
        {
            return new TextChatModel(credentials, initialInfo, this);
        }

        public IObservable<IContact[]> GetContactList(ISessionCredentials user)
        {
            var whenContactListReceived =
                SendRequest<ContactList>(GetUri(_baseServer, @"Network/GetMyNetwork.ashx"), new GetContactListRequest(user), "ContactList")
                .Do(contactList => { if (contactList.InvalidSSID != null) throw new InvalidOperationException("Server has returned «InvalidSSID» error code."); });

            var result =
                from contactList in whenContactListReceived
                where contactList.Users != null
                select contactList.Users;
            Contract.Assume(result != null);
            return result;
        }

        public IObservable<IContactListResponse> AddContact(ISessionCredentials user, IUserInfo contact)
        {
            var result =
                SendRequest<ContactListResponse>(GetUri(_baseServer, @"Network/AddToMyNetwork.ashx"), new AddContactRequest(user, contact), null);
            Contract.Assume(result != null);
            return result;
        }

        public IObservable<IContactListResponse> DeleteContact(ISessionCredentials user, IUserInfo contact)
        {
            var result =
                SendRequest<ContactListResponse>(GetUri(_baseServer, @"Network/DelFromMyNetwork.ashx"), new DeleteContactRequest(user, contact), null);
            Contract.Assume(result != null);
            return result;
        }

        public IObservable<IUserSearchResponse> SearchUsers(Language? knownLanguage, Language? practicingLanguage, bool? chatsOnline, Country? country, int? minAge, int? maxAge, string firstName, string lastName, string location, IUserInfo lastViewedUser)
        {
            var request = new UserSearchRequest(knownLanguage, practicingLanguage, chatsOnline, country, minAge, maxAge, firstName, lastName, location, lastViewedUser);
            var result =
                SendRequest<UserSearchResponse>(GetUri(_baseServer, @"Members/SearchMembers.ashx"), request, null);
            Contract.Assume(result != null);
            return result;
        }

        private static readonly string[] FullPackList = new string[] 
        { 
            "Languages", "Locations", "Flash.Main", "Flash.Genders", "Flash.YesNo", "Flash.Loader", 
            "Flash.Main.ContactUsSubjects", "Flash.Main.HardCoded", "Flash.Main.SubAcceptEULA", 
            "Flash.Main.SubHome", "Flash.Main.SubLogin", "Flash.Main.SubNeedBirthDate", "Flash.Main.SubNewUser", 
            "Flash.Main.SubRetrieveCreds", "Flash.Months", "Flash.SubAboutUs", "Flash.SubActivation", 
            "Flash.SubChatHouse", "Flash.SubMyMailbox", "Flash.SubMyProfile", "Flash.SubNewMail", 
            "Flash.SubVoiceChat", "Flash.AdsLandingBox", "Flash.SubLinks", "Flash.SubMyNetwork" 
        };

        private static readonly string[] ShortPackList = new string[]
        {
            "Languages", "Locations", "Flash.Main", "Flash.Genders", "Flash.YesNo"
        };

        public IObservable<LanguagePacks> GetLanguagePacks(string languageCode, bool allPacks)
        {
            Contract.Requires(!String.IsNullOrEmpty(languageCode));

            var packRequests = from n in allPacks ? FullPackList : ShortPackList select new GetLanguagePacksRequest.LanguagePackRequest(languageCode, n);
            var request = new GetLanguagePacksRequest(packRequests.ToArray());
            var result =
                SendRequest<LanguagePacks>(GetUri(_textChatServer, "LanguagePacks/GetPacks.aspx"), request, null);
            Contract.Assume(result != null);
            return result;
        }

        private static readonly Regex ResponseAdsExpression = new Regex(@"<DIV .*</DIV>", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
        private static string RemoveAdsFromResponse(string responseString)
        {
            Contract.Requires(responseString != null);
            Contract.Ensures(Contract.Result<string>() != null);

            string result = ResponseAdsExpression.Replace(responseString, String.Empty);
            return result;
        }

        private IObservable<TResponse> SendRequest<TResponse>(Uri requestUri, IShredRequest request, string outerElementName) 
            where TResponse : class 
        {
            Contract.Requires(requestUri != null);
            Contract.Ensures(Contract.Result<IObservable<TResponse>>() != null);

            IObservable<string> whenResponseReceived =
                request != null ?
                WebHelper.PostHttpRequest(requestUri, Serialize(request), Encoding.UTF8, Encoding.UTF8) :
                WebHelper.GetHttpRequest(requestUri, Encoding.UTF8);

            var result =
                from rawResponseString in whenResponseReceived
                let responseString = RemoveAdsFromResponse(rawResponseString)
                let wrappedResponseString = String.IsNullOrEmpty(outerElementName) ? responseString : String.Format("<{0}>{1}</{0}>", outerElementName, responseString)
                select new XmlSerializer(typeof(TResponse)).Deserialize<TResponse>(wrappedResponseString);
            Contract.Assume(result != null);
            return result;
        }

        /// <summary>
        /// Сериализует переданный объект в XML-подобную строку
        /// </summary>
        /// <param name="obj">Сериализуемый объект</param>
        /// <returns>Результирующая строка</returns>
        /// <remarks>
        /// Стандартную сериализацию не удалось настроить нужным образом.
        /// </remarks>
        private string Serialize(object obj)
        {
            Contract.Requires(obj != null);
            Contract.Ensures(!String.IsNullOrWhiteSpace(Contract.Result<string>()));

            var serializedObj = new StringBuilder();
            using (var writer = XmlWriter.Create(serializedObj, new XmlWriterSettings() { OmitXmlDeclaration = true }))
            {
                writer.WriteStartElement(obj.GetType().GetCustomAttribute<XmlRootAttribute>(true).ElementName);

                var serializableProperties =
                    from p in obj.GetType().GetProperties()
                    let attrAttr = p.GetCustomAttribute<XmlAttributeAttribute>(false)
                    let elemAttr = p.GetCustomAttribute<XmlElementAttribute>(false)
                    let textAttr = p.GetCustomAttribute<XmlTextAttribute>(false)
                    where attrAttr != null || elemAttr != null || textAttr != null
                    select new { Property = p, Attr = (Attribute)attrAttr ?? (Attribute)elemAttr ?? (Attribute)textAttr };

                var properties =
                    from p in serializableProperties
                    let v = p.Property.GetValue(obj, null)
                    where v != null
                    select new { Property = p.Property, Attr = p.Attr, Value = v };

                Func<object, string> objToString =
                    o =>
                    {
                        Contract.Requires(o != null);
                        Contract.Ensures(Contract.Result<string>() != null);

                        if (o is string)
                            return (string)o;
                        else if (o is int)
                            return ((int)o).ToString(CultureInfo.InvariantCulture);
                        else if (o is Int64)
                            return ((Int64)o).ToString(CultureInfo.InvariantCulture);
                        else if (o is Guid)
                            return ((Guid)o).ToString();
                        else if (o is int?)
                        {
                            var v = (int?)o;
                            if (v.HasValue)
                                return v.Value.ToString(CultureInfo.InvariantCulture);
                            else
                                return null;
                        }
                        else
                            throw new NotSupportedException(String.Format("Type {0} is not supported.", o.GetType().FullName));
                    };

                var attrProperties =
                    from p in properties
                    let a = p.Attr as XmlAttributeAttribute
                    let valueString = objToString(p.Value)
                    where a != null && valueString != null
                    select new { AttributeName = a.AttributeName, ValueString = valueString };
                attrProperties.ForEach(i => writer.WriteAttributeString(i.AttributeName, i.ValueString));

                if (properties.Any(i => i.Attr is XmlElementAttribute))
                    throw new NotSupportedException("Serialization of properties as XML elements is not supported.");

                var textProperty = properties.SingleOrDefault(i => i.Attr is XmlTextAttribute);
                if (textProperty != null)
                {
                    var value = objToString(textProperty.Value);
                    writer.WriteValue(value);
                }
                else if (obj is IEnumerable)
                {
                    ((IEnumerable)obj).Cast<object>().ForEach(
                        i =>
                        {
                            var value = Serialize(i);
                            writer.WriteRaw(value);
                        });
                }

                writer.WriteEndElement();
            }

            string result = serializedObj.ToString();
            Contract.Assume(!String.IsNullOrWhiteSpace(result));
            return result;
        }
    }

    [XmlRoot("UI")]
    public sealed class InitialTextChatInfo : IInitialTextChatInfo
    {
        [ContractInvariantMethod]
        void ObjectInvariant()
        {
            Contract.Invariant(Uid >= 0);
        }

        [XmlAttribute("UID")]
        public int Uid { get; set; }

        [XmlAttribute("GUID")]
        public Guid Guid { get; set; }

        private static readonly ShortUserInfo[] EmptyUsers = new ShortUserInfo[0];
        private ShortUserInfo[] _onlineUsers = EmptyUsers;
        [XmlElement("U")]
        public ShortUserInfo[] OnlineUsers
        {
            get { return _onlineUsers; }
            set { _onlineUsers = value ?? EmptyUsers; }
        }

        IShortUserInfo[] IInitialTextChatInfo.OnlineUsers { get { return OnlineUsers; } }
    }

    [XmlRoot("Data")]
    public sealed class InitialTextChatInfoWrapper
    {
        [XmlElement("UI")]
        public InitialTextChatInfo InitialTextChatInfo { get; set; }
    }
}
